How to list all subcollections of a Cloud Firestore document?
As detailed in the Cloud Firestore documentation, data in Firestore is stored into documents, which are “organized into collections”. Documents can contain subcollections which, in turn, can contains documents.
The documentation also indicates that:
Documents in subcollections can contain subcollections as well, allowing you to further nest data. You can nest data up to 100 levels deep.
Normally, as a Firestore database architect, while working out your data model, you decide the ids of the different subcollections of a document.
For example, for a Restaurants Reviews application, you may decide to have a reviews
subcollection for each restaurant
document¹.
And, with the JavaScript SDK, you would query all the review
documents for a specific restaurant
document with an id
of 123
as follows:
db.collection("restaurants").doc("123").collection("reviews").get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
console.log(doc.id, " => ", doc.data());
});
});
However, in some specific cases, it may happen that subcollections are created dynamically, by the users, while using the application.
For example, let’s imagine a CRM application where users create Customer Visit reports. Each user of the application has a user’s document in the Firestore database. For each customer visit, the corresponding visit report is saved in a subcollection of the user’s document which has the id of the customer².
The figure below illustrates this data model, showing the user1
document with two subcollections corresponding to two customers: CUST_1234
and CUST_6541
.
Each of these two collections was created the first time user1 created a document within the collection (i.e. created a visit report for the corresponding customer).
Now, imagine that, in the application, you need to list these collections, for example to allow the user selecting the client for which he/she wants to list the visit reports.
The question is: How do you get this list of subcollections, from the application?
If you look a the Firestore documentation, you will see that:
Retrieving a list of collections is not possible with the mobile/web client libraries.
Fortunately, there are several possible workarounds to get a list of (sub)collections from a web/mobile app. Let’s detail a couple of them!
Approach #1: Save the list in a dedicated field of the parent document
As the title says, this approach consists in having a field in the subcollections’ parent document that holds the subcollections ids. When you want to get the list, you simply fetch the parent document and read the value of this field. The best is to use a field of type Array.
How to populate this field?
Simply use arrayUnion()
, as explained in the documentation, here and here, and as shown in the example below (JavaScript SDK):
Note that, since we don’t know upfront if the subcollection id has already been added to the array field, we need to update this field each time we write a new document in the subcollection, multiplying the write cost by two³.
An alternative would be to read the parent document before writing to the collection, to check if the subcollection id is already present in the array. But, in addition to generating an extra “roundtrip” to the database, it would incur the cost of one document read.
Another drawback of this approach, is the fact that you need to manage the case when all the documents of a subcollection are deleted. This is more complex than it seems: you either need to maintain a documents counter for each subcollection or, worst, need to query the entire subcollection to count the documents (using the get()
method and the size
property) …
In other words, this first approach, which at first glance seems promising, implies quite a lot of extra reads and writes, which in turn generate some additional cost…
Fortunately (again!), there is another possible approach.
Approach #2: Use a Cloud Function
While it is not possible, with the mobile and web client libraries, to retrieve the list of subcollections of a document, it is possible with the Cloud Firestore server client libraries.
Therefore, it is possible to use the Cloud Firestore Node.js Client API to write a Cloud Function that lists the subcollections of a document.
Since we will call this Cloud Function from the app we use a Callable Cloud Function⁴.
Here is the code of the Cloud Function:
As you can see it is quite simple. Let’s detail each line of this code⁵.
- We import the Cloud Functions and Admin SDK modules using Node
require
statements. - We initialize an
admin
app instance. - We use
functions.https.onCall()
to create the Callable Cloud Function. This method takes two parameters:data
andcontext
(optional). - We use the
data
parameter to getdocPath
, the value of the Firestore document path (slash-separated). This value is passed from the client calling the Cloud Function (see below). - We then call the asynchronous
listCollections()
method on theDocumentReference
created by usingdocPath
(i.e.admin.firestore().doc(docPath)
). ThelistCollections()
method returns a Promise that resolves with an array ofCollectionReference
s. Note thatlistCollections()
fetches only the subcollections that are direct children of the document. - We then use the
map()
method to create a new array with theid
s of the subcollections. Theid
property of aCollectionReference
s holds the last path element of the referenced collection. - Finally, we send back to the client a JavaScript object that can be JSON encoded and that contains the array of subcollection
id
s.
Calling this Cloud Function from the client is even easier. Here is the code for the JavaScript SDK, in order to call it from a web application:
Here is how it works:
- We declare an
HttpsCallable
, which is “a reference to a callable http trigger in Cloud Functions”. When called, it returns a Promise that resolves with anHttpsCallableResult
, that, in turns, wraps a single result. - We then call it, passing the Firestore document path (
collectionId/documentId
) in an object. - We finally use the
data
property of theHttpsCallableResult
to get thecollections
array returned by thegetSubCollection
Cloud Function.
We can then do whatever we want with this array: writing it to the console, or looping over it and printing each collection id, or even looping over it and fetching, for each collection (i.e. for each element of the array), all the documents of the collection, etc.
Note that it works similarly with the Android and iOS SDKs (see the documentation) as well with FlutterFire (see the documentation).
That’s it! We now have a way to get all the subcollections of a given Firestore document, from a client (Web, Android or iOS). And the subcollections array is immediately adapted if a subcollection is added or deleted.
In addition, this solution does not imply any extra document read or write.
It only costs one Cloud Function call, which is not expensive ($0.40/million of invocations). Moreover Firebase offers a generous free tier of “2,000,000 invocations, 400,000 GB-sec, 200,000 CPU-sec, and 5 GB of Internet egress traffic” each month. For more details on the exact costs, see here and here.
You will find in the following github repository the code of a small Firebase project which includes:
- The Cloud Function code, as presented above;
- A simple HTML page that demonstrates how it works from a web client.
Deploy it to one of your Firebase project (see the readme file) and open the root url of the project (https://<your-project-id>.firebaseapp.com) with your preferred browser.
Just enter a document path in the dedicated field and click the button “Get Subcollections”:
- If the document has one or more subcollections the page will display their id(s).
- If the document at the path does not exist or if it does not have any subcollection, the Cloud Function will return an empty array.
If you have any question or suggestion, please leave a comment below.
#BetterTogether
[1] Actually this is one of the examples used in this official Firestore video https://youtu.be/v_hR4K4auoQ
[2] Of course, this is just one of the possibilities for modeling this specific case. Other approaches may be totally valid! The idea is just to use this data model as an example of subcollections with ids assigned dynamically by the users.
[3] I.e. one write for the subcollection document and one write for the update of the parent document.
[4] We could have chosen an HTTPS Cloud Function, but Callable Functions have several advantages compare to HTTPS ones, as explained in the doc.
[5] Note that some parts of the explanation text that follows are directly copied/pasted from the Firebase documentation!